Skip to content

v0.8.18: table ttls, perf improvements, egress management, desktop app improvements, tools APIs, permission groups enhancements, sailpoint integration, files keyword search - #7381

Merged
waleedlatif1 merged 92 commits into
mainfrom
staging
Sep 2, 2026

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

TheodoreSpeaks and others added 30 commits August 29, 2026 03:47
#7275)

A Slack-triggered run's subject is the external Slack user, and a schedule or
public-API run has no subject at all. list_groups, list_people, and send_invite
demanded a Sim user, so every unattended run got "Credential Group user access
required" — including reads that need no actor. Authority for an actorless
caller comes from the deployment the workspace layer already checks.

Invitations no longer name an inviter when there is no person to name, rather
than borrowing the run's actor and claiming someone invited when they did not.
* feat(function): mount referenced files into the code sandbox

Reference a file's path in Function block code and it is mounted for you:
`<block.file.path>` resolves to its location on the sandbox filesystem, so
any language can open it. It is the counterpart to `.base64`, which inlines
the bytes and only works in JavaScript, leaving Python and Shell with no way
to read a referenced file at all.

Resolution happens long before a sandbox exists, and mount paths are only
assigned once the whole set is planned together, so the resolver leaves a
marker that the function runtime swaps for the real path — the same shape as
LargeValueRef.

Files written to /tmp/sim/outputs come back as platform file objects, so they
can be attached or uploaded with no intermediate step. Harvesting is automatic
on runs already in the remote sandbox; isolate runs pay nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(execution): cover the sim.* helpers in a real isolate

isolated-vm.test.ts mocks the spawn, so nothing proved the sim.* namespace
was reachable from user code — only that the process plumbing was called.
These run the real worker and assert values cross the boundary both ways.

Pins the fast runtime's global surface, which turned out narrower than
assumed: plain ECMAScript plus fetch, console and sim.*, with no Buffer,
require, process, crypto, TextDecoder, atob or setTimeout. That list is
exactly what decides whether a block needs an import and so moves to the
remote sandbox, so it is asserted rather than described.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: regenerate tool metadata and integration docs

Changing function_execute's params and adding file_write's fileInput made
both generated artifacts stale, which check:audits catches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(function): address review findings on sandbox file I/O

- Classify harvested output by content, not by file name. Gating the
  provenance scan on a filename-derived MIME type let a resolved secret be
  written as plaintext under a binary-looking extension and skip the only
  guard before upload. Bytes that round-trip as UTF-8 are scannable
  whatever they are called.
- Format the .path replacement through the shared context-aware helper.
  Returning a bare identifier inserted __blockRef_N literally in Shell and
  inside quoted strings instead of the mounted path.
- Enforce the mount ceiling on the combined set. Marker-derived mounts
  bypassed the contract's max, which only bounded the explicit files param.
- Reuse one marker per file key, so referencing a path twice mounts once.
- Raise a coded error when the code deletes the output directory, so it
  reports as a 400 with remediation rather than an opaque 500.
- Drop declared paths from the discovered set; counting a file in both
  rejected a single output larger than half the byte ceiling.
- defineProperty when rebuilding context values, so an own __proto__ key
  survives instead of hitting Object.prototype's setter.
- Give the directory sentinel a collision-resistant name, and cap
  file_write's fileInput at the destination's own limit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(function): include files in the FAQ's output answer

Adding <function.files> to the outputs table left the FAQ still saying the
block returns two outputs, and the reference FAQ never mentioned that a
file is read by referencing its path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(function): scan every harvested output for resolved secrets

Gating the scan on whether the bytes looked textual was defeatable twice
over: name the file .png, or append a single invalid byte, and a plaintext
secret skipped the only guard before upload.

A lossy UTF-8 decode preserves ASCII runs, so a literal secret is findable
in any buffer. The scan is now unconditional. What remains out of reach is
a secret carried in transformed form, which no substring scan can see —
an inherent limit of scanning rather than a hole in the gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(function): correct provenance, mount caps and runtime file plumbing

Review findings from the sandbox file I/O change, traced to root cause.

A file_write carrying fileInput copied its bytes into a new workspace
file without the source's secret lineage, so a file the platform had
locked as secret-derived became readable again under a new id. It now
derives provenance from the source through the same helper archiving
uses, which also marks a source with no workspace row unknown rather
than empty.

The tool half of that feature had shipped without the block half: the
File block's write mapper listed four params and dropped fileInput, and
no sub-block could supply one, so the documented binary path was
unreachable from the canvas and from an agent holding the File tool.
Adds the canonical pair, maps it, and drops content's unconditional
required now that the two are mutually exclusive sources.

The harvest counted secret names the matcher had already discarded as
too short to identify anything, so a workflow whose every secret was
under the substitutable-literal minimum built no matcher, classified
every output unknown, and refused each one while claiming it held a
secret. The counter now applies the matcher's own predicate.

The execution file index was lazily created on the per-call context
clone, so files a tool produced were recorded onto a throwaway and the
next call in the run never saw them. Materializing it on the source
before the spread makes both objects share one map.

A .path reference inside a quoted Python or JavaScript string was
JSON-encoded, putting literal quote characters inside the path the code
then opened. Mount paths are sanitized to a safe character set, so they
splice raw.

URL mounts now carry a ceiling that curl enforces on the bytes actually
served, rather than trusting a recorded size, and declared sandbox
outputs opt out of the harvest explicitly instead of relying on which
branch returns first. Also stops rebuilding non-plain context values
into stripped objects, and removes an agent tool-description branch that
no longer had a sub-block to read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): send one write source, and bound mounts by what they were charged

The write card sent `content` unconditionally, and the contract counts any
defined `content` as "text was provided" — so an untouched Content box,
which serializes as an empty string, collided with a selected file and
rejected every binary write. The mapper now emits only the source the card
carries, leaving the contract to catch filling both or neither.

A generated document that references other files needs a principal to
resolve them; without one the resolver can only serve an already-published
artifact and throws. The write path now passes it.

URL mounts are granted exactly the byte count they were charged against the
aggregate, rather than being charged a reported size while permitted the
global per-file maximum — twenty mounts each claiming a byte could
otherwise be allowed 500MB apiece. An honest size fetches normally and an
understated one is refused.

`--max-filesize` only refuses a transfer up front when the response
declares a Content-Length, so a chunked reply slipped past it. The
delivered file is now measured and removed if it overran, in the same
command rather than a second round trip.

Also documents that naming an explicit sandbox output path excludes the
harvest directory, in both the block docs and the tool description the
model reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): accept the picker shape, and bound a mount while it downloads

The write path required an already-complete UserFile, but the file picker
stores {name, path, key, size, type} with no id or url — so selecting a
file in the new basic field was rejected as not a file object before any
bytes moved. It now runs through the same normalizer every other operation
in that file applies to its own input, while a block reference or an
agent-resolved id still passes through as-is.

Writing a deliberately empty text file was also rejected, because an empty
string read as "no text". The selected file is what disambiguates now: with
one present an empty Content box means unused, and with none, content
always goes through.

--max-filesize only refuses a transfer that declares a Content-Length, so
the previous size check ran after the whole object had already been written
to sandbox disk. The fetch now streams through a byte cap, so at most one
byte over the limit can ever land, and curl's status travels through a file
so a 403 on an expired URL is still distinguishable from an empty download.

A non-finite mount size made every comparison false, so the aggregate check
passed while the mount was charged the per-file maximum anyway; the size is
resolved once now, before either test.

Mount resolution failures are the caller's files — unreadable, oversized,
or over the aggregate — and now answer 400 with the message naming the
file, rather than a 500. Files already uploaded when a later one in the
same harvest is refused are removed, since the export is all-or-nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): clamp a declared mount cap, and demand identity not full metadata

sandboxFiles reaches the sandbox layer from the request body, so a declared
maxBytes is a caller's number. It may now lower its own mount's ceiling but
never raise it past the one that layer guarantees.

The write path required full UserFile metadata, but size is never read
before the download and the download reports the real content type — so a
reference carrying id, key, url and name was rejected over two fields
nothing depends on. It now asks only for identity and fills the rest.

Uploads already made are also discarded when a later upload throws, not
only when a later file is refused for carrying a secret. Both exits leave
the harvest all-or-nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): answer null for a key a file input cannot be classified by

fileInputToUserFile normalizes caller-supplied file objects and returns
null for anything it cannot use — but it classified the storage key with
the throwing form, so a key without a recognized context prefix escaped as
a 500 from every operation that normalizes a file input, not just write.

Adds tryInferContextFromKey beside inferContextFromKey, sharing the one
prefix list so a new context cannot be added to half of them. The throwing
form stays right where an unclassifiable key means the platform built one
wrong; the nullable form is for keys that arrived in a request, where an
unrecognized prefix only means this is not a file we can use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The admin workspace move was restricted to personal/grandfathered sources;
`assertWorkspaceMovable` refused anything already owned by an organization, so
support could only re-home a workspace with manual SQL.

Relax that guard to a drift-only check and handle the source organization.
`changeWorkspaceStoragePayerInTx` already accepted an arbitrary source payer,
so the storage-ledger rebalance needed no change.

Moving a workspace between organizations is the first operation capable of
separating an artifact from the organization that owns it, so two invariants
nothing has ever had to defend are enforced here:

- A custom block and its bound workflow always share an organization.
  `getCustomBlockAuthority` resolves by the consumer's org and
  `admitCustomBlockChildExecution` skips its concurrency reservation on the
  strength of that, so a stranded row would run a foreign tenant's workflow
  under its owner's credentials, billed to the wrong payer. The move
  unpublishes those blocks through the product's own `deleteCustomBlock` and
  records the loss in the source organization's audit view.
- A fork parent and child always share an organization. `resolveForkEdge` has
  no org check at all, so the move refuses while a cross-org edge would result.

Move-time checks alone cannot hold either invariant, because the writers can
commit after them. `publishCustomBlock` now validates and inserts under the
organization mutation lock the move holds, and `createFork` row-locks its
parent so the move and the organization-attach path — both of which take
`FOR NO KEY UPDATE` on that row — serialize against it.

Pending invitations block: re-stamping an org-scoped invitation would convert
a pending membership in the source org into one in the destination, consuming
a seat for an invitation the destination never issued.

An entitlement downgrade blocks. When entitlement is subscription-backed, both
organizations are re-evaluated under the locks rather than trusting a
pre-transaction read that is stale in both directions. The two modes where
`resolveOrganizationEnterprisePlan` grants entitlement by deployment
configuration are excluded through a shared `isSubscriptionBackedEntitlement`
predicate, so a missing subscription row is never misread as a lapse.

Both organizations are locked, ascending by id, mirroring
`acquireOrganizationUserMutationLocks`. The source id is read optimistically
before the transaction and re-verified under the locks, retrying through the
existing loop when it moved.
…orkflows Beyond HubSpot and Zapier (#7280)

* feat(library): AI Agents for Marketing Automation: Building Agentic Workflows Beyond HubSpot and Zapier

* fix(library): use Chat and Sim instead of the prohibited Mothership name

The constitution reserves "Sim" for the agent and "Chat" for the surface;
"Mothership" names no product surface a reader can find.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Sim Pi Agent <pi@sim.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…7281)

* fix(provenance): let a run that never started report why it failed

A copilot-run workflow that fails before reaching the engine crossed
back with no provenance, which latched the tool's registry and reduced
the result to "result unavailable". The caller was told its run failed
but not that the workflow was undeployed, or the input invalid, or the
slot unavailable — the reasons this layer produces before any block
runs, naming no secret because none had been resolved yet.

The executor attaches its execution result to every throw, so the
absence of one is proof that no block ran: output, logs and error are
all undefined and the only content is a message this layer wrote. That
is an absence, not an inability to vouch, so the crossing now carries
an exact-empty envelope. The message still passes the tool boundary's
egress projection against the same registry, so anything that registry
knows is still redacted. A run that did execute and could not vouch
hands back its incomplete envelope exactly as before, and that still
latches.

Make the attach total rather than conditional to keep that inference
sound. A block failure is already normalized on the way in, so the old
`instanceof Error` guard held in practice; what it did not give was a
guarantee covering a non-Error raised by the engine's own synchronous
work. toError is identity-preserving, so ordinary failures keep their
type.

The empty envelope moves to the registry module, which owns the
vocabulary, replacing a private copy in the logging session so one
definition states what "vouched for, naming nothing" is.

* fix(provenance): keep the post-run crossing window out of the never-started claim

Review round 1, both findings accepted.

The post-run crossing runs inside the same try as the executor call, so
when that import is what throws, the catch sees an error carrying no
execution result — the same evidence a run that never started leaves.
The previous condition read that as "nothing crossed" and vouched for
it, when in fact an execution exists and its provenance was never
imported, which is exactly the content that cannot be vouched for.

Record whether the executor returned and require both facts before
claiming the absence: not past the executor, and no result attached.
Everything else hands back whatever envelope it has, and an incomplete
one still latches.

The executor test also could not fail against the old gated attach: a
block failure is normalized on the way in, so its rejection already
arrived as an Error. Drive it through the cancellation subscribe run()
awaits before the queue instead, which is its own synchronous work and
reaches the catch untouched — the case the total attach exists for.

* fix(provenance): carry the run's result through post-execution failures

Round 2, cubic's finding accepted — and it was a distinct window, not a
restatement of round 1. The executor's post-execution work runs after
the run has produced a result but before `executeWorkflow` returns, so
a failure there reached callers with no result attached: the run threw
nothing itself, and the flag added last round could not be set yet.
Every consumer that reads a missing result as "no block ran" was wrong
in that window, this crossing included.

Fix it where the result lives rather than at each reader. The executor
attaches its own on the throws it raises; `executeWorkflow` now does the
same for failures raised after it holds one, skipping the case the
executor already recorded. Logging and trace spans get the same benefit
for free — they read the identical signal.

That makes an absent result total again, so the boolean flag goes and
the crossing reads one thing: the result from the error, or the one
already returned when the failure came later still, from the crossing
itself. Only a failure with neither can claim nothing ran. The
post-return case now describes content with the run's real envelope
rather than latching blind, which is strictly more accurate than either
prior behaviour.

* fix(provenance): normalize a post-execution failure so it can carry the result

Round 3, cubic's finding accepted. The guard added last round required
the caught value to already be an `Error`, so a non-Error raised by
post-execution work skipped the attach and was rethrown bare — the same
hole this branch closed in the executor, left open one layer up by my
own change. A Copilot run would have reported an executed workflow as
never started and vouched for content it cannot describe.

Normalize once at the top of the catch and use that value throughout,
including the rethrow, matching what the executor does. `toError`
returns an `Error` unchanged, so a custom error class keeps its
identity and every ordinary failure is untouched — the existing
identity assertion on the rejection path still holds.

Two tests: the result reaches an ordinary post-execution failure, and a
non-Error one is normalized so it can carry the result too. The second
fails against the previous guard.
* feat(billing): record function sandbox usage

* fix(billing): charge function user-code failures

* fix(billing): correct sandbox trace cost boundaries

* fix(billing): tighten sandbox completion boundaries

* test(billing): check the metered sandbox amount against a real provider

The pricing unit test pins the arithmetic and the conformance suite proves a
cost is produced, attached to the right outcomes, and routed — but that suite
stubs the provider and mocks Date.now() with a counter advancing one
millisecond per call. Under that clock `total > 0` is the strongest claim
available, and it holds equally well if the metered window is anchored to the
wrong instants or the resource constants are wrong.

Bounds the charge between what the sleep must cost and what the wall clock
could justify, so a wrong rate, a wrong vCPU/memory constant, and a
mis-anchored window all fail. Provider-agnostic via resolveProvider, opt-in
behind SANDBOX_BILLING_SMOKE=1 like the sibling smoke suites.

Verified against both providers: E2B billed 9.031s of a 9.302s call at
$0.1656/hr, Daytona 8.435s of 8.721s at $0.16668/hr — both matching published
rates, both excluding ~275ms of Sim-side overhead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(billing): meter the sandbox a cloud Pi session runs in

Pi's own sandbox was never metered. withPiSandbox called createSandbox
without the meterUsage argument, so only sandboxes created through
executeFunctionRequest were charged — and Pi's is the larger consumer by an
order of magnitude. A Function block holds one for seconds; a Pi session
holds one for a minimum lifetime of 31 minutes.

The gap was worst exactly where it was least visible. A Pi coding agent
normally runs BYOK, so its model cost is zero by definition, and the ledger
bills a model row on total > 0. With the sandbox unmetered, such a run
produced a zero-cost model_unbilled row and Sim collected only the flat
execution fee while paying its provider for the whole session.

Threads a cost sink through PiRunContext, which is the seam backends
already receive and the only one that reaches all four cloud modes. The
handler owns one sink covering both sandbox sources — Function tools in
local mode, the agent's own sandbox in cloud mode — so neither can be
dropped where the cost is folded into the block's output. It rides in
toolCost for the same reason the Function tool cost already does: that is
what survives the BYOK zeroing.

Unlike the Function path this charges on creation rather than on a
completed session. A Function run is seconds long, so absorbing one the
provider failed to deliver is cheap and reads as fair; tens of minutes of
Pi compute is consumed whether the agent finished, errored, or was
cancelled, and billing only clean endings would mean paying for every other
one. A create that throws still costs nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(billing): check the Pi sandbox charge against a real provider

The handler test mocks the backend and writes into the cost sink by hand,
so it proves the wiring from a backend to the block's cost and nothing
more — it would still pass if withPiSandbox never metered at all, which is
precisely the bug that path had.

Holds a real Pi sandbox open for a known interval and bounds the charge
between what that interval must cost and what the whole session could
justify. Verified to fail against the original unmetered call with
"expected 0 to be greater than or equal to 0.00023", and to pass once the
sink is threaded: 5.949s billed of a 6.141s session on E2B.

The second case pins the other half of the contract — a caller that
supplies no sink is not charged, which is what keeps mothership and other
internal Pi sandboxes free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(billing): charge a Pi session only when it completed

Aligns Pi with the outcome policy the Function path already applies rather
than keeping the divergence the previous commit introduced. A session that
ends by throwing — a provider crash, a lifetime limit, a cancellation — is
absorbed, because a charge nobody can tie to delivered work is not one
worth defending, and consistency across the two sandbox paths is worth more
than recovering the cost of runs that failed.

A command exiting non-zero is still billed: the callback returns normally
there and the agent produced its answer, which is the same reason the
Function path bills its own non-zero exits.

The window still closes at teardown, so a completed session is charged for
the whole time the provider held its sandbox.

Verified on both providers, including that the new case fails when the
charge is applied unconditionally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(billing): keep the charge on completed runs that fail after execution

Three paths dropped cost the sandbox had already earned.

A harvest that cannot return what the run produced — more files than the
export limit, nesting past the listing depth, or an output directory the
code deleted — was excluded from the billable-error set. All three arrive
only after the sandbox has executed and all three are the caller's to fix,
so they belong with the post-completion export failures the policy already
bills rather than the provider failures it absorbs. A completed run whose
code wrote one file too many went free.

That also left the route with nothing to read: it already consults
readTrustedSandboxOutputCost for these errors, so attaching the cost at the
sandbox layer is what carries it into the response.

Separately, a Function block whose handler succeeded could still fail in
the steps that follow it — base64 hydration, and large-value redaction that
throws rather than emit unredacted data. Those errors carry no cost of
their own, so the completed sandbox went unbilled. The handler's cost is
now held across that window, in the same way streamingPartialOutput already
is, and used only when the error has none.

The new conformance case was confirmed to fail against the narrower catch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(billing): carry the Pi charge onto a session its agent failed

A backend that returns a result carrying `totals.errorMessage` has already
run: the sandbox was billed and the sink holds the charge. But that path
throws instead of reaching `buildOutput`, which is what publishes the cost,
so the charge was accumulated and then dropped — lost revenue rather than
an over-charge.

Both failure paths now carry it on the error they raise, the same way the
Function handler carries its tool cost, so `handleBlockError` can pick it
up. An agent that ran and then reported a failure consumed the same tokens
and sandbox seconds as one that succeeded, which is why the cost
computation is now shared between the two rather than duplicated.

Also corrects the sink's doc comment. Local mode does fill it — the agent
runs on the caller's own machine and costs Sim nothing, but a
`function_execute` among the Sim tools it calls bills its own remote
sandbox into the same total.

The new case was confirmed to fail without the attach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(tables): add column type extension points

* Address PR review feedback (#7119)

- preserve explicit nulls from source-owned conversion normalization
- normalize hooked values before select migration
- cover null and select conversion rewrites

* refactor(tables): short-circuit unlimited column types

* refactor(tables): keep CSV coercion in import switch

* test(tables): cover rebased column dropdown
* feat(tables): add row TTL expiration

* chore(api): regenerate table API artifacts

* fix(tables): make TTL cleanup fair across tables

* fix(tables): reject rolled-over TTL dates

* fix(copilot): remove unrelated catalog drift

* chore(helm): bump chart for TTL cron

* fix(ci): regenerate CLI table restore type

* refactor(tables): keep TTL CSV parsing in import switch

* docs(tables): clarify TTL epoch-second storage

* docs(tables): clarify TTL cleanup timing

* fix(tables): prioritize oldest TTL cleanup rows

Page cleanup by created_at and id, with a supporting index, so capped runs make progress on the oldest rows first.

* improvement(tables): rename TTL column to Expiration

* chore(tables): run TTL cleanup every fifteen minutes

* feat(tables): gate row TTL expiration

* improvement(tables): align Expiration feature messages

* chore(db): format TTL migration metadata

* test(tables): cover TTL column dropdown

* fix(tables): close TTL extension gaps

* fix(tables): keep TTL import coercion lightweight
* fix(timezone): consolidate table wall-clock conversion

* fix(timezone): preserve ambiguous date semantics

* fix(tables): prevent early TTL expiration

* fix(tables): wait for timezone before TTL edits

* improvement(tables): improve timezone loading state

* test(tables): preserve editor timezone during edits

* fix(tables): guard row modal timezone edits
* feat(tables): trigger workflows on row deletes

* fix(tables): bound delete trigger snapshots

* fix(tables): scope and verify delete triggers
…e integration's output, timeout, and redirect defects (#7276)

* fix(elasticsearch): resolve a Cloud ID to the real Elasticsearch host

* fix(elasticsearch): reject a malformed parent-domain port in a Cloud ID

* fix(elasticsearch): correct get_index output, cluster-health timeout, and redirect credentials

* fix(elasticsearch): reject a surviving colon in a decoded Cloud ID component

* fix(elasticsearch): stop a cloud deployment falling back to a stale host
…7284)

* improvement(integrations): refresh Slack integration page SEO copy

* fix(integrations): correct Slack trigger name and tool categories in SEO copy
appVersion has been pinned at v0.7.44 since chart 1.2.0 while the app moved
through forty-odd releases. It is the default tag for every first-party image
(app, realtime, migrations, pii, copilot), so a helm install that does not pin
image.tag deploys an application far behind the chart shipping with it — and
any values key added by a newer chart is silently inert, because the running
image has no code that reads it.

The release tag is published by the main-branch merge commit that cuts it, so
this lands on main together with v0.8.18.

The kind install test also stops resolving its images through appVersion. That
job installs the default configuration, so it pulled the tag appVersion names —
which, on the very PR that raises appVersion, has not been published yet. The
install would sit in ImagePullBackOff until --wait timed out. Pinning CI to the
published :latest removes the circularity that kept appVersion frozen.
)

A .chart document is author-controlled and rendered with setOption() straight
into the app document, including on the anonymous /f/<token> share route.
confineOptionToCanvas closed the innerHTML/document.write paths but left the
navigation ones open: title.link, title.sublink, and a link on a treemap or
sunburst data item each reach windowOpen, which assigns the URL to
location.href, so a javascript: URL executed on the app origin on one click.

Drop the link keys everywhere in the walk alongside toolbox. A chart has no
reason to navigate its viewer, so they are stripped rather than
scheme-checked, which would still leave an open redirect on an authenticated
origin.
Two ceilings on a Function run's sandbox files were charged per source
rather than per execution.

Mounts: planUserFileMounts assigned a path per element, so one storage
key named by two sources became two mounts. `files` is `user-or-llm`
and deduped nowhere, so a model repeating an id — or naming a file the
code also references with `<block.file.path>` — produced a duplicate
that cost a presign, a second transfer of identical bytes, and a second
charge against both the byte budget and the 20-file mount ceiling,
either of which then refuses a request that fits. Collapse by storage
key, first occurrence wins. The contract already requires a non-empty
key, so there is no keyless case to carry.

Exports: MAX_SANDBOX_OUTPUT_FILES is documented as what one execution
may export "whether declared by path or discovered by harvesting", and
collectExportedFiles already runs the byte ceiling that way. The count
ceiling did not, so a request declaring paths and harvesting a directory
could export 20 of each. Count declared and discovered together, with a
declared path inside the directory dropped from the discovered set so it
is not billed on both sides. With no declared paths — every call
execute-request makes, since it sets outputSandboxDir only when nothing
declares a sandboxPath — the check and its message are unchanged.

The resolver's marker reuse is no longer what keeps a twice-referenced
file to one mount; its comment said otherwise.

Fixture keys in sandbox-mounts.test.ts were identical across files the
tests meant to be distinct; they now differ, which is what those tests
always claimed to set up.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…redentials (#7290)

* fix(admin): report a completed workspace move's true source and its credentials

The durable move payload records `sourceOrganizationId` as a tri-state: absent
means the operation predates the field, an explicit `null` means the workspace
came from a personal source. The payload parser collapsed both to `null`, so
every reload or retry of a personal-source move claimed its source organization
had failed to persist.

The applied and reloaded responses also blanked the credential summary. Unlike
the source impact, those rows are workspace-scoped and travel with the move
untouched, so an admin who had just confirmed a move was told the workspace
carried no secrets, environment variables, or BYOK keys.

* test(admin): cover the deleted source organization and truncated credential counts

`getSourceOrganization` was mocked inline in the module factory, so the reload
branch that reports a recorded-but-deleted source organization could not be
exercised at all. Hoist it like the other mocks and pin that third tri-state.

Every credential fixture also reported zero dropped rows, so the applied and
reloaded truncation records would not have caught a regression to the hardcoded
zeros they replaced.
* feat(files): add workspace content search

* fix(files): address search review findings

* chore(helm): bump chart for search scheduler

* fix(db): sync file search migration snapshot

* fix(files): address second search review round

* fix(files): harden search and content provenance
* chore(sandbox): remove obsolete image task routing

* fix(files): dispatch search indexing from Trigger workers
* feat(library): Best AI Agents for Lead Enrichment in 2026

* fix(library): remove markdown from FAQ answers

---------

Co-authored-by: Sim Pi Agent <pi@sim.ai>
Co-authored-by: Waleed Latif <walif6@gmail.com>
…params (#7287)

* fix(github): wire the block fields whose ids do not match their tool params

* chore(github): use TSDoc for the new wiring comments

* fix(github): treat boolean false as an explicit gist visibility choice

* fix(github): scope each param alias to the operations that declare it

* docs(github): record why an absent operation degrades to a no-op

* test(github): pin the gist visibility and list-filter outcomes

* test(github): exercise the isSet guard instead of short-circuiting past it

* test(github): drive the wiring assertions from the alias table itself

* test(github): remove the param-wiring suite
* fix(timezone): preserve low-year wall clocks

Avoid Date.UTC's 1900 remapping and retain four-digit years through date and TTL editing.

* fix(timezone): fall back from invalid saved zones

* fix(tables): report rejected TTL imports

* fix(timezone): reject empty zone identifiers

* fix(tables): dispatch row delete triggers asynchronously

* fix(tables): cap rows to delete snapshot budget

* fix(tables): signal partial TTL cleanup changes

* fix(tables): wait for timezone before date edits

* fix(tables): preserve blank TTL values

* fix(tables): guard date edits against invalid timezones

* fix(tables): address timezone and delete review findings

* fix(timezone): format automatic timezone label
* fix(tables): run workflow groups from deployments

* fix(tables): validate deployed workflow mappings

* fix(tables): pin table workflow deployment state

* fix(tables): pin deployed workflow variables

* fix(tables): pin workflow group deployment versions

* fix(tables): follow latest workflow deployment

* fix(tables): remove copilot deployment mode

* fix(tables): resolve canonical workflow starts

* fix(scripts): simplify staging workflow backfill

* fix(scripts): skip deleted table workflows

* fix(scripts): avoid private Redis in staging backfill

* fix(scripts): skip locked table workflows

* fix(scripts): support production workflow backfill

* fix(scripts): skip undeployable table workflows
* feat(files): add an overwrite option to file write

* fix(files): keep an overwrite create on its exact path

* docs(files): describe overwrite as an exact-path match
* fix(chat): scope selection clipboard chips to workspace

* fix(chat): keep table clipboard scope current

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
#7306)

* fix(incidentio): align declared tool outputs and enum hints with the v2 API

Four declared outputs could never be populated because the v2 API names the
field differently, and three param descriptions advertised enum values the
API rejects. Verified against https://api.incident.io/v1/openapiV3.json,
which serves the v1/v2/v3 schemas in one document.

- IncidentV2 has `permalink`, not `incident_url`, and no `description` at
  all: incidents list/create/update/show now declare and map `permalink`
  and drop `description`.
- IncidentUpdateV2 has `new_incident_status` (required), not an optional
  `new_status`; `message` is optional, `updated_at` is not returned, and
  `updater` is an actor wrapper whose user lives under `updater.user`.
- EscalationV2 has `title` and `status`, not `name`.
- WorkflowsCreate/UpdateWorkflowPayloadV2 accept only `newly_created` and
  `newly_created_and_active` for `runs_on_incidents`, and
  CustomFieldsCreatePayloadV2 accepts only text, link, numeric,
  single_select and multi_select for `field_type`.

Renaming a declared output changes the reference-picker surface for these
tools; nothing in the repo read the old names.

* fix(incidentio): stop the block offering rejected enum values and declare every actor branch
…7307)

Okta's Management API spec uses two distinct user path parameters. `pathId`
("An ID, login, or login shortname ... of an existing Okta user") backs
`/api/v1/users/{id}` and every `/api/v1/users/{id}/lifecycle/*` operation.
`pathUserId` and `pathAppUserId` ("ID of an existing Okta user") back the
factors, roles, sessions, and membership paths.

Every Okta `userId` param is `user-or-llm`, so its description is the only
thing a model reads before choosing what to pass. Eight tools on `pathUserId`
endpoints advertised "User ID or login", so a model that supplied an email got
a 404; `delete_user` sits on a `pathId` endpoint but promised an ID only, so a
model resolved an ID it never needed.

Tightened: assign_user_role, list_user_roles, remove_user_role, enroll_factor,
list_factors, get_factor, reset_factor, clear_user_sessions.
Loosened: delete_user.

A registry-derived test classifies each Okta tool by the path its own `url`
builder produces, so a future tool is covered without a hardcoded list.
…s endpoint (#7308)

* fix(github): make branch protection usable and wire workflow_id to its endpoint

`github_update_branch_protection` could not be called. `restrictions` was a
required tool param with no subBlock anywhere in the block, so every run threw
"Restrictions is required for GitHub Update Branch Protection" before the
request. The other three body fields were `required: true` behind `mode:
'advanced'` subBlocks that are hidden by default, and their subBlock types did
not match their param types.

GitHub documents all four as required but nullable - "Set to null to disable" -
so "required" there means present in the body, which `null` satisfies. Sim's
`required: true` means the user must supply a non-empty value, which is strictly
stronger. The params are now optional and the body builder supplies the explicit
`null` GitHub demands for each field left unset, normalizing the editor's JSON
strings and the dropdown's 'true'/'false' along the way. A `restrictions`
subBlock is added.

`workflow_id` rendered for `github_list_workflow_runs` but the tool declared no
such param and always hit the repo-wide `/actions/runs`. It now selects
`GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs`, which GitHub
documents as accepting the same query filters.

No subBlock id is renamed or removed and no visibility is flipped.

* fix(github): name the branch-protection parse failure without echoing the input
* feat(sso): add safe member provisioning

* fix(sso): keep subscription admission consistent

* fix(sso): harden provisioning edge cases
icecrasher321 and others added 23 commits September 1, 2026 11:14
…ock renderer (#7336)

* fix(tool-input): render every tool param through the canonical sub-block renderer

An agent block's tool row had grown a second, lossy copy of the sub-block
renderer and a second value encoding that nothing decoded.

A tool param with no matching sub-block fell into a hand-rolled renderer that
ignored `param.type` and produced a text box — which is why Jira's boolean
`includeAttachments` rendered as free text, where typing `false` enabled it.
~2.7k of ~26k user-facing params took that path.

Separately, `StoredTool.params` stringifies every value while the canonical
`Switch` stores a real boolean, so a switch set to off reached the tool as the
truthy string `'false'` — 409 switches across 99 blocks. The editor lied too:
`Boolean('false')` rendered it back on after every remount.

One producer, one renderer, one type map, one encoding:

- `tools/param-shape.ts` holds the single value-type -> control map and the
  encode/decode pair, as a leaf module with no registry edge. It absorbs
  `subBlockTypeForField`, which answered the same question in a second
  vocabulary.
- `getSubBlocksForToolInput` returns real + synthesized `SubBlockConfig`s, so
  every user-facing param renders through `SubBlock`. Claims are computed from
  the block's full sub-block list, so a param whose sub-block condition fails
  stays hidden instead of reappearing as a text box.
- The decode keys off the SUB-BLOCK, not the tool's declared type. Jira's
  `deleteSubtasks` is a dropdown of 'true'/'false' backing a boolean param, and
  the block compares `=== 'true'`; keying off the tool type would have inverted
  that flag and ~167 like it.
- `buildBlockToolParamsTransform` is shared by the agent block, Pi local tools
  and Human v2, which each previously applied a different subset.

Deleted: `renderParameterInput`, `ParameterWithLabel`, `ToolCredentialSelector`,
`getToolParametersConfig`, `ToolParameterConfig`, `UIComponentConfig`,
`resolveSubBlockForParam`, and the search indexer's parallel branch.
`workflow-input-mapper` became a real sub-block type instead of a hard-coded
`workflow_executor` branch.

checkbox-list was broken on every surface: it wrote each option id as its own
top-level store key, so the serializer dropped them all. Verified against the
real serializer — jina's and pinecone's options never reached their tools, and
asana's reached a `params` fn that tested `Array.isArray` on a boolean. It now
stores one record under its own id and projects onto its option params at both
boundaries; an untouched option is omitted rather than sent as `false`, which
`update_task` depends on.

Human block v2: running notification tools through the full transform changes
what an already-configured tool receives, so it ships as `human_in_the_loop_v2`
with v1 hidden and sunset. The ten sites keyed on the block type now share one
`isHumanInTheLoopBlock` predicate.

A persisted `'false'` now means `false`. Affected workflows have been doing the
opposite of what their editor showed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tool-input): address review — decode MCP structured args and custom-block fields by declared type

Six review findings, all real:

- `getJsonSchemaValueShape` round-tripped through the control it picks, and an
  `object` renders in a code editor whose store value is raw JSON text — so the
  shape came back `string` and MCP object/array args reached the server
  undecoded. It now reads the schema directly.
- `mcp-dynamic-args` picked its control from the normalized union type but still
  read raw `paramSchema.type` for `requiresJsonValue`, the slider step/rounding,
  the numeric check and the array placeholder. A nullable object took the
  non-JSON path; a nullable integer got a fractional step. `jsonSchemaType` is
  exported and every read goes through it.
- A custom block's inputs were not decoded on the executor: the server overlay
  builds its configs with `inputFields: []`, so `blockDef.subBlocks` carries no
  field sub-blocks. The assembler now takes `binding.inputFields`, and the
  resolved-secret projection reads the same list off the provider tool config so
  both copies decode identically. Keyed on the DECLARED field type, since
  `number` and `object` render as controls that store strings.
- Pi local tools failed open when the projection transform threw after the real
  one succeeded, pairing transformed execution params with untransformed
  projected ones. Now marks the registry incomplete and refuses, matching
  `prepareToolExecution`.
- 15 sub-blocks declare no `title`, so removing the formatted-id fallback left
  them unlabelled in a tool row. Restored in the producer.
- `deserializeBlock` now rebuilds a checkbox-list record from its option params,
  the inverse of the serializer projection, so the round trip stays lossless.

Two of my own tests had encoded the first bug and were corrected rather than
kept green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tool-input): address review round 2 — enum shape and legacy MCP arg values

- `getJsonSchemaValueShape` checked the enum branch before the declared type, so
  `{ type: 'integer', enum: [1,2,3] }` was shaped as text. The dropdown it renders
  as stores `String(option)`, so the server received '1' instead of 1. The
  declared type now wins, and an untyped enum is inferred from its members.
- An MCP argument entered before its control was derived from the NORMALIZED
  schema type is stored as a string, so a union-typed boolean's 'false' would
  tick the switch it now renders as. `mcp-dynamic-args` decodes on read; already
  typed values pass through, so it is a no-op thereafter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tool-input): regenerate icon mapping and guard an empty MCP enum

- `docs:check` failed: adding `human_in_the_loop_v2` (and hiding v1) left the
  generated icon maps stale. Regenerated; the diff is only those entries.
- `enumMemberShape` read an empty enum as numeric, because `every` is vacuously
  true on `[]`. A third-party MCP schema can send one.
- The MCP dropdown's highlight label used a truthiness check, so a falsy member
  (`0`, `false`) rendered blank now that the value is decoded rather than kept
  as a string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tool-input): address review round 3 — checkbox write race and MCP enum member types

- `CheckboxList` merged onto the value captured at render, so two toggles landing
  before React rerendered both built from the same record and the second write
  dropped the first. Every option in the group now shares one key, so it reads the
  store at write time; the store updates synchronously, so it always sees the
  preceding toggle. The old per-option keys could not hit this.
- The MCP dropdown wrote back the string the combobox works in, so a numeric enum
  sent '1' for 1, 'true' for true and 'null' for null. It now persists the enum
  MEMBER, matched by its stringified form, so nothing downstream has to guess.
- Selection is decided by key presence rather than truthiness, so `0`, `false` and
  a literal `null` member render as selected instead of blank.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(mcp): recover enum members from legacy stringified arguments

The dropdown now persists the enum member, but arguments saved before that
still hold `String(member)` text. Only the member list inverts that encoding —
a value shape cannot tell `'null'` the text from `null` the member, and the
declared-type coercions never see an untyped property.

- `jsonSchemaEnumMember`/`decodeJsonSchemaValue` in `param-shape` own the
  inverse, next to the `buildSubBlocksFromJsonSchema` encoder it undoes.
- `coerceToolArguments` applies it at the MCP execution boundary, which is the
  one place both the MCP block and an agent's MCP tool row pass through, so
  the editor and the server agree on one value.
- `validateToolArguments` accepts a declared member on its own: `enum` is
  narrower than `type`, and a recovered `null` satisfies no type branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(mcp): read tool schema properties through the shared normalizer

Reading `properties` straight off the wire meant the enum lookup — which runs
before `hasType` so it can recover an untyped or `null` member — dereferenced
whatever the server sent, and `properties: { foo: null }` threw out of
`validateToolArguments`, where nothing catches it.

`jsonSchemaProperties` already answers "what does an untrusted property look
like" for every other reader, so both loops go through it instead of guarding
per call site. That drops the local `isDeclaredEnumMember` copy and the
redundant `inputSchema?.properties` early return: one normalizer, one
`jsonSchemaEnumMembers` accessor, no property access ahead of a guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* revert(mcp): drop the enum-member recovery layer

Checked against the prod replica rather than reasoning about it, and every
case this guarded has zero instances:

- `workflow_mcp_tool`: 470 properties, 0 declare an `enum` at all.
- MCP blocks' cached `_toolSchema`: 6 enums, all `type: 'string'` with string
  members. No untyped enum, no `null`/number/boolean member.
- Stored MCP block arguments (179): 0 stringified booleans, 0 stringified
  nulls. Booleans and numbers are already persisted with their real types.
- Agent MCP tool-row params (10,022): 0 stringified booleans, 0 stringified
  nulls, 4 stringified numbers — already covered by the `type: number`
  coercion that predates this branch.

For a string enum `String(member) === value` returns the same string, so the
whole path was a no-op on every row in production while adding a coercion at
an authorized use case plus a validation widening to keep that coercion safe.
The dropdown already persists the member (679325a), so nothing downstream
has to reverse the encoding.

`enumMemberShape` goes with it — every real enum declares a type, so member
inference never runs. The declared-type-first ordering stays: it is a
precedence fix, not a layer. `getJsonSchemaValueShape` is private again.

`lib/mcp/application/execute-tool.ts` is byte-identical to 679325a.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(permission-groups): regenerate the block successor map for Human v2

`check:block-successors` arrived with the staging merge and this branch is
what makes it stale: versioning the Human block adds a successor edge the
generated map did not carry.

Not cosmetic — the map is how a permission group that allows `human_in_the_loop`
keeps covering `human_in_the_loop_v2`. Without the edge, an enterprise group
scoped to the old block would deny its replacement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e-workflows room (#7356)

* feat(sidebar): live workflow and folder registry updates via workspace-workflows room

* fix(sidebar): cover v1 workflow import fan-out and scope folder invalidation per review
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
* fix(selectors): close residual cancellation gaps

* fix(selectors): recheck post-resolution cancellation

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
* improvement(audits): eliminate repeated parsing and scans

* fix(audits): resolve local bins under vitest

* fix(audits): include drizzle subpath imports
…7365)

* feat(library): Sim: The Open Source Zapier Alternative for AI Agents

* Pi Babysit: address PR #7365 feedback

---------

Co-authored-by: Sim Pi Agent <pi@sim.ai>
* fix(selectors): paginate HubSpot owner options

* fix(selectors): preserve hydrated HubSpot owner IDs

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
* fix(confluence): normalize space identifiers

* fix(confluence): hydrate legacy numeric space selections

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
* feat(ashby): expand hiring operations and webhooks

* test(ashby): type webhook fixtures

* fix(ashby): harden uploads and pagination

* fix(ashby): correct API contracts and webhook handling
…te across CI runners (#7367)

* perf(tests): cut per-file import graphs in apps/sim and shard the suite across CI runners

* fix(tests): recheck abort after lazy handler load; list script tests by file
* fix(deps): preserve runtime and canvas behavior

* fix(demo): validate Cal embed configuration

* fix(deps): preserve runtime compatibility
* chore(skills): teach babysit about cubic, not just Greptile

The loop only re-triggered `@greptile` after a push. Two bots review this repo,
so a round would end with Greptile at 5/5 and cubic's threads still open against
an earlier commit, their findings never re-checked against the fix.

Adds what distinguishes them: cubic has no score and reports only inline
threads, posts a fresh review per run rather than editing one comment in place,
answers to `@cubic-dev-ai review this PR` rather than `@cubic`, and reviews the
commit that was HEAD when its run started — so a stale thread is a
reply-and-resolve, not a fix.

Both are now re-triggered as separate comments and confirmed `pending` before
the loop waits, since a trigger that silently failed is indistinguishable from a
reviewer that has not started.

Also promotes CI to a stop condition. A red required check is not clean no
matter what either reviewer says, and the lint and audit jobs routinely catch
what a local run does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(skills): treat a pending check as unfinished, not as passing

Two defects in the round this adds, both caught in review.

The stop condition tested only for a failing check, so a check still running
when both reviewers finished counted as clean and the loop reported the PR
ready before CI had its say. It now requires every check to have finished and
passed, and sends a pending one back to the wait step.

The thread-attribution note pointed at `author.login` per thread, but
`PullRequestReviewThread` has no author field — GitHub rejects the query with
`Field 'author' doesn't exist on type 'PullRequestReviewThread'`. Identity
lives on the comments, so it now names the real path and says not to lift the
field to the thread level.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
* feat(file): search workspace files by regular expression

Search read its query as literal text. It now reads it as a line-oriented
regular expression by default, with a Match setting on the block to go back
to verbatim text.

The segment store and its `gin_trgm_ops` index already support this: pg_trgm
extracts trigrams from a regex source too, so `~` / `~*` plan as a bitmap
index scan exactly like `LIKE` / `ILIKE`. No migration, no new index.

One compiled pattern owns every mode-specific decision — how PostgreSQL
matches a segment, whether the segment must hold a whole line, and where the
match sits inside it — so the repository builds one query shape and the
preview renderer one preview shape. Compilation happens in the application
use case, not the route adapter, so every surface gets the same semantics.

The supported syntax is the intersection of PostgreSQL ARE and JavaScript
RegExp, because the same source drives both the indexed predicate and the
client-side match location a preview centres on. Anything the two engines
read differently is rejected by name rather than silently reinterpreted, and
`\b` is rewritten to `\y` on the way to PostgreSQL.

Safety, in four independent layers:

- A pattern must contain 3 consecutive literal characters every match will
  include. pg_trgm indexes nothing shorter, and an unextractable pattern
  plans as a sequential scan across every workspace's segments.
- `new RegExp` proves it compiles in JavaScript.
- PostgreSQL proves it compiles in ARE; 2201B becomes a 400, not a 500.
- `statement_timeout` bounds the read. This one covers exact matching too,
  which has always been able to reach the same scan through a
  punctuation-only or non-ASCII query.

`mode` is a builder setting, withheld from the model like `maxResults`. The
model cannot see it and the two readings disagree on every metacharacter, so
`toolEnrichment` replaces the declared syntax with the active mode's — a
regex sent to a block set to exact matching would otherwise be searched for
verbatim and silently find nothing.

Verified against PostgreSQL 17: 14 behavioural checks end to end, a live
search over 150,012 segments in 14ms, 6/6 representative patterns reaching
the trigram index, and the guard cutting a 12s pattern at 10.08s into an
actionable message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): locate regex matches in PostgreSQL, never in JavaScript

Preview rendering ran the user's compiled pattern with `RegExp.exec` to centre
the excerpt on the match. `RegExp` matches by backtracking, and the literal-run
gate admits nested quantifiers, so `(a+)+bcd` against a long segment cost 768ms
at 40 leading `a`s and doubles with each one — synchronously, on the event loop,
once per returned row, and entirely outside the statement timeout that bounds
the query which found the row.

PostgreSQL runs that same pattern in 0.49ms: its engine does not backtrack, and
`regexp_instr` runs inside the read's transaction, so locating a match can never
cost more than having found it. Regex mode now selects the match offsets
alongside the row and `findMatchRange` returns null for it, which is the
interface's contract rather than an omission. Exact mode is unchanged — scanning
for a known string is linear.

PostgreSQL counts characters where JavaScript slices by UTF-16 unit, so the
offsets are converted by walking the segment rather than assuming either width.

Also fixes two audit failures: `getErrorMessage` in place of a hand-written
`instanceof Error` ternary, and regenerated tool metadata and integration docs
for the search params.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): accept open-ended repeats, count characters, classify lock waits

Three defects from review, none of which the tests caught:

`{n,}` was rejected. `readQuantifierAt` reports an unbounded maximum as
Infinity, and the repeat cap compared it directly, so every open-ended repeat
failed as "exceeds 1000" — a form the tool's own documentation offers. Only a
stated maximum is measured now, and the minimum always is, since that is what
an expansion unrolls.

Query bounds and literal runs were measured in UTF-16 units while claiming
characters, so two astral characters read as four and slipped a gate written
for three. Both now count characters, which is also what pg_trgm indexes.

`lock_timeout` was set without classifying what it raises. A wait on
conflicting DDL surfaced as an unclassified server error, and folding it in
with the timeout arm would have told the caller to fix a pattern that is
already correct. It now maps to a distinct error the caller is told to retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): keep long previews honest, credit forced repeats, drop bad hints

Four review findings, each reproduced before it was changed.

A regex match has no length limit, so `abc.*` on a long line produces a match
larger than the whole preview budget. The layout passed it through whole and
let the final byte cap cut it, which removed the closing marker along with the
text — 2048 bytes of output ending mid-line with nothing to say so. The match
is now clipped against a budget that reserves that marker, and a clipped match
always carries one.

A variable repeat was scored at one occurrence when its minimum forces more:
`(?:ab){2,5}` cannot match without `abab` in it, but the run was counted as 2
and the pattern rejected against a gate of 3. It now contributes the copies its
minimum forces.

`\Y`, `\m` and `\M` were rejected with a suggestion to write `\b`, `^` or `$`.
Those are different assertions — a non-boundary, and two word edges rather than
the line's — so the hint handed back different semantics as a fix. They now say
no supported escape means the same thing. `\y`, `\A` and `\Z` keep theirs,
which are genuine.

Smart case was documented as reacting to any uppercase letter, but it reads
literals only, so `\D` and `[A-Z]` do not make a search case-sensitive. The
tool, block and generated docs now say what the code does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): credit runs across a repeat, count matching lines not matches

The tool promised "each match" while the query is distinct on file and line,
so several matches on one line return one row. An agent reading the contract
would have expected otherwise; it now says each matching line once.

A repetition of a non-fixed atom was scored at what one copy guarantees, but
from two copies on its own tail and head meet: every match of
`(?:a(?:x|y)bc){2}` contains `bca`, which neither copy contains alone. That
run is now credited, so patterns the index can serve are no longer rejected.

Scores are capped alongside the strings they measure. Joining two capped
strings yields twice the cap, which `concatenate` could already exceed — the
gate never noticed, since it only compares against three, but the bound is
documented and now holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): reject overflowed repeat bounds, describe what search covers

An upper bound too large for `Number` arrives as Infinity, and the exception
that lets `{n,}` skip the repeat cap could not tell the two apart — so
`needle{1,<400 digits>}` passed the cap that `needle{1,5000}` fails. The
quantifier now records whether a bound was written at all, and a written one
must be at or under the cap however large it is.

The tool promised every active workspace file. It searches what the index
currently holds: a file still pending, failed, or skipped as unsupported is
not searched, and an agent reading "every file" would take an empty result as
proof of absence. Both descriptions now say so and point at `complete` and
`indexStatus`, which already carry the detail.

The declared query description spoke only for regex mode, which is what the
catalog and the generated docs render — so a builder using exact matching was
told to write a regular expression and to obey a rule that does not apply to
them. It now names both readings; the runtime schema is still enriched with
whichever is in force. The docs overview said literal text, which stopped
being true when regex became the default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(file): close the allowlist around PostgreSQL bracket expressions

`[:class:]` was rejected while `[=equivalence=]` and `[.collating.]` were
forwarded unchanged. All three are PostgreSQL bracket expressions with no
JavaScript counterpart, and the parser exists to admit only what both engines
spell the same way — so two of them passed an allowlist whose whole point is
to close, and were accepted by documentation that says POSIX classes are not
supported.

They are now rejected by the construct they open, each named in its own error.
An ordinary class holding a literal dot, `[.]` or `[a.b]`, is untouched: the
form only matches on a bracket nested inside a class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(selectors): expose incomplete loaded catalogs

* fix(selectors): cover incomplete comparison catalogs

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Waleed Latif <walif6@gmail.com>
…I shards (#7375)

* perf(ci): run apps/sim as three test shards

* chore(deps): drop unused @types/mdx devDependency from apps/docs

Nothing in apps/docs imports mdx/types or a bare *.mdx module; the
generated .source imports carry a ?collection= suffix that the *.mdx
wildcard does not match, and @types/mdx still resolves transitively via
@mdx-js/mdx for fumadocs. The docs type-check passes without the direct
edge.

* perf(tests): move DOM-free jsdom suites to the node environment

Audited the 229 test files declaring @vitest-environment jsdom. 217 need a
real DOM (React rendering, TipTap/ProseMirror, window/document/localStorage,
DOMParser, ProgressEvent) and stay on jsdom; 12 exercise pure logic (yjs
collab-doc conversion, http tool URL building, panel editor store, log
utils, attachment upload, headless TipTap table commands, terminal-session
tooltip helpers) and now run on node, skipping the jsdom boot.

Dropped one redundant test in apply-streamed-markdown.test.ts: the
hand-rolled one-shot peer case is a strict subset of the NON-OVERLAPPING
two-peer test in apply-streamed-markdown.concurrent.test.ts, which also
asserts CRDT convergence and exactly-once survival of the peer edit.

* perf(tests): drive retry backoff and scheduled flushes with fake timers

Real sleeps and repeated setup were the run-phase cost in seven files:

- tools/index.test.ts: `retryDelayMs: 0` falls through to the 500ms default, so
  every retry test slept for real. Fake the clock for the retry describe.
- embeddings/client.test.ts: two retry tests waited the 1s initial backoff.
- hosted-key-rate-limiter.test.ts: the queue-head test slept two 200ms polls;
  the abort test slept 20ms wall clock and asserted on Date.now.
- event-buffer.test.ts: sleep(60)/sleep(5) waited on the 15ms flush timer.
- events.attribution.test.ts: walked and read the whole app twice, serially.
  Walk once in a beforeAll with batched concurrent reads.
- sso-trust.test.ts: imported the Better Auth graph in each of three tests.
  Import once in a beforeAll that carries the 30s budget.
- markdown-parse.test.ts: the one-shot helper built and destroyed a tiptap
  Editor per document; reuse one across the 400-seed property corpus.

No production timing constants, corpus sizes, or assertions changed.

* perf(tests): drop the executable tool registry and full block map from non-sweep tests

The vitest suite is bounded by module loading, and the biggest single cost is a
test file opting out of the global `@/tools/registry` or `@/blocks/registry`
mock. Trim that where the test never needed the whole thing:

- `lib/internal/tool-operations/registry.server.test.ts` checked registration
  against `getToolIds()` but still imported the executable registry for one
  sweep. That sweep — every operation-backed tool has no `request` and a
  registered handler — is the in-process half of the transport partition, so
  it moves next to the external-URL sweep in `tools/request-transport.test.ts`,
  which already pays for the registry. Handler loads now run in parallel.
  Collect 12.5s -> 0.1s, run 6s -> 4.3s; request-transport unchanged (~8s).
- New `partialBlockRegistry` in `@sim/testing/mocks/block-registry.mock`
  builds the `@/blocks/registry-maps` shape from the block modules a test
  actually reads, so the real registry code runs over those blocks alone.
  Applied to the six non-sweep tests that unmocked the block registry for one
  or two blocks (webhook, slack, whatsapp, condition/function,
  knowledge/start_trigger): collect 3-8s -> 0.05-2s each. Registry sweeps
  keep the real map.
- `tools/index.test.ts` retry cases set `retryDelayMs: 0`, which the retry
  config replaces with the 500ms default; `1` is the smallest delay honored.
  Run 4.0s -> 0.45s.
- `tools/metadata.test.ts` collects sweep offenders into one assertion instead
  of one `expect` per registry entry, and indexes ids once. Run 1.1s -> 0.03s.
- The `vi.unmock('@/tools/registry')` in the search-replace tests was dead:
  nothing in their graph imports the registry any more, and all 106 tests pass
  without it, contrary to the comment that claimed eight would fail.

* perf(tests): stop copilot handler tests from loading the executor, block, and trigger registries

The router test stubs every handler implementation module so the handler map
binds without loading the executor and block registry; the 90s timeout goes.
Handler, VFS, payload, and request tests mock the execution-side leaves and
catalog registries they never exercise. Source edges that dragged in barrels
for a single symbol now import the defining module: the Mothership block icon
from @sim/emcn/icons, contract schemas from their contract file, and autolayout
constants from new @sim/workflow-renderer/dimensions and /note-content subpaths.

lib/copilot: 141 files, per-file collect+run 74.2s -> 37.4s, wall 19.1s -> 15.0s
router.test.ts: 15.1s -> 0.45s

* fix(tests): import ContractBodyInput from the contract types module

* fix(tests): bind the real cancellation handler in the router test; mock the narrowed contract path

* fix(emcn): wait for focus restoration instead of a fixed 20ms sleep in the modal test
* feat(resources): remember list preferences

* fix(resources): honor explicit default list params

* fix(resources): harden list preference checks

* improvement(resources): simplify list preference adapters
…could not be run (#7374)

* feat(tools): run one integration tool over v2, and fix the ones that could not be run

Adds `POST /api/v2/tools/{toolId}/execute` and `sim tools execute <toolId>`,
then fixes the 119 tool parameters that made the same call impossible through
Copilot.

The catalog already published every tool's parameters and outputs; there was
no way to supply them. The only execution endpoint on v2 was
`POST /workflows/{id}/execute`, so a caller could read that `gmail_send` takes
`{to, subject, body}` and still had to wrap it in a workflow to send the mail.

Building it surfaced the second half. Copilot authorizes integration tools when
their schemas are built — `projectIntegrationToolsForViewer` decides what the
model is told exists — so by dispatch the id has been vouched for. An endpoint
whose caller types the id has to make those decisions against it, which is what
the use case does, and which is how the parameters below came to light.

117 of them are mislabels: mailchimp and zendesk mark a user-typed API key
`hidden`, which firecrawl spells `user-only` for the identical shape, and
pipedrive and wealthbox omit the `oauth` declaration that fills the
`accessToken` they correctly hide. The other 2 are not — calcom's `attendee`
and mistral's `file` are composed by their block at serialization time, so
`hidden` was right until a caller could reach the tool directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(mistral): align MistralParseV3Block on the tool's `file` parameter

`check-block-registry.ts` requires every required `user-only` tool parameter to
have a subBlock whose `id` or `canonicalParamId` matches it, because the
serializer resolves those by direct lookup and a mismatch false-flags the field
as missing at submit time. V3's subBlocks carried `canonicalParamId: 'document'`
against a tool parameter named `file`, bridged by the block's params mapper.

Aligning them needs no migration. Saved state is keyed by subBlock id
(`fileUpload`, `fileReference`), which is unchanged; `canonicalParamId` is a
config-derived index rather than a storage key. `data.canonicalModes` is keyed
by canonical id, but `backfillCanonicalModes` re-derives a missing entry from
whichever value is populated, so a workflow using the advanced file-reference
field still resolves to `advanced` on open.

V2 keeps `document`: only V3's `file` parameter is `required`, so only V3 is
under the contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): reject a direct call missing a required user-only input

`validateRequiredParametersAfterMerge` checks `user-or-llm` parameters alone,
because on the workflow path a `user-only` parameter was already validated
during serialization against the block field that holds it. The v2 execute path
has no serialization step, so nothing had checked them: omitting
`zendesk_get_ticket`'s `subdomain` reached Zendesk as `undefined` and came back
a provider authentication failure — the same undiagnosable shape this branch
set out to remove.

The check exempts a parameter Sim supplies itself, mirroring
`injectHostedKeyIfNeeded`'s three tests in the same order so the two cannot
disagree about whether a value is coming. `firecrawl_scrape` stays callable
with no `apiKey` where keys are hosted, and requires one where they are not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): require every input the caller is the only source for

The previous commit gated the check on `user-only`, which reads the visibility
taxonomy as if it constrained who may send a value. It does not. `visibility`
describes editor roles — a human filling a block field, the agent block's model
choosing an argument, either, or neither — and a direct call has no editor and
no agent block, so those roles collapse to one caller.
`createUserToolSchema`, which this endpoint and Copilot's
`call_integration_tool` both publish, already says so by omitting `hidden` and
nothing else.

So the rule is not about roles: Sim supplies it, or the caller must. Skipping
`hidden` stays safe because `check-tool-param-reachability` fails any required
hidden parameter without a declared filler.

Concretely this closes `thinking_tool.thought`, the one required `llm-only`
parameter in the registry, which the narrower check let through as `undefined`.
It also moves required `user-or-llm` inputs to a pre-dispatch failure naming
every missing field at once, rather than the merge validator's first-failure
mid-execution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): address the cubic review round on direct tool execution

Two were real defects rather than polish:

The usage ledger de-duplicates on `eventKey`, and the derived key hashes actor,
workspace, source and description — identical for every call to the same tool.
`onConflictDoNothing` therefore billed the first hosted-key call and silently
dropped every one after it. A workflow run has an `executionId` to separate its
rows; a direct call has nothing, so it now mints one.

Undeclared input keys reached the executor. `impersonateUserEmail` is read
straight out of params and forwarded to credential-token resolution as an
impersonation request, and no tool declares it. The two ad-hoc denylists are
replaced by a declared-parameter allowlist — the actual boundary, and the one
`GET /api/v2/tools/{toolId}` already publishes.

Also: renaming a canonical id orphans `data.canonicalModes`, and the backfill
recovers it only when one side holds a value. `setBlockCanonicalMode` writes the
mode without clearing its sibling, so a workflow holding both would have
silently switched from the typed reference to the uploaded file.
`migrateCanonicalModeIds` carries the key across, ahead of the backfill.

Smaller: the reachability audit now mirrors execution exactly, exempting
`credentialType` only under `authoritativeParams` and refusing to treat
conditional hosting as a guarantee; the boundary guard splits on both path
separators so it still excludes the execute route on Windows; the route stops
documenting the 409 and 423 it cannot produce; and `sim tools execute` prints
the output it promised in the human formats.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): bill only spend that was Sim's, and scope the boundary predicate

Metering read `output.cost.total` and billed it. That field is not a hosted-key
marker: `knowledge_upload_chunk` and the enrichment runner report their own cost
there, so a workspace would have been charged `api-tool` spend for something
already metered elsewhere, and a failed call carrying a cost billed too. The
registry writes hosted-key cost under exactly two conditions —
`hostedKeyInfo.isUsingHostedKey && finalResult.success` — and metering now
matches them.

`hostedKeyParamFor` derives the first of those the way `injectHostedKeyIfNeeded`
does, including the test the earlier version omitted: a caller's own key wins,
so supplying one means Sim's key was never spent. The same helper answers the
other question that turns on it, since a required parameter Sim fills is not a
missing one.

The boundary predicate matched `execute` anywhere in the absolute path, so a
checkout living under a directory of that name would have excluded every
catalog route and retired the guard silently. Now derived from the path relative
to the app, still split on either separator for Windows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): refuse a caller-supplied hidden parameter

Declared was being treated as accepted. A `hidden` parameter is Sim's to fill —
a resolved credential's `accessToken`, a hosted key, a block-composed shape —
and `createUserToolSchema` omits it from what this endpoint and Copilot
publish. Accepting it anyway either let a caller pre-empt the executor's value
or silently discarded theirs when the executor overwrote it; either way the
published schema made no such promise.

The accept-set is now exactly the publish-set: a key is taken if and only if
`GET /api/v2/tools/{toolId}` lists it as something the caller may send. This is
the rule the required-input check already followed ("Sim fills it, or the caller
must"), applied to the other direction.

Verified live against a dev server: a forged `accessToken` on `gmail_read_v2`
is refused before dispatch, and the credential-backed read still succeeds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): honor a tool's declared credential selector, and refuse its aliases first

Sixty-eight tools — Snowflake among them — declare the credential selector as a
required `user-only` parameter (`oauthCredential` or `credential`) with no
`oauth` block, filled by their block from an `oauth-input` field. The
required-input check ran against the raw body, before the top-level
`credentialId` had been placed anywhere, so a valid credential was rejected as
a missing `oauthCredential`. And the alias refusal ran only over *undeclared*
keys, so `input.oauthCredential` on such a tool passed the declared-key check
and bypassed the top-level field — credential precedence differing per tool.

The credential is still named once, at the top level. It now lands under
whichever selector the tool declares (or `credential`, which the executor reads
for OAuth resolution, when it declares none), and required inputs are validated
against what the executor will actually receive. A declared required selector
also demands `credentialId` up front, the same as an `oauth` block does. The
alias refusal is unconditional and runs first.

Verified live: a Snowflake call with a top-level credential passes the
validator and fails downstream at resolution; `input.oauthCredential` is
refused; omitting the credential names `credentialId` as required.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): read the registry's hosted-key verdict instead of re-deriving it

The metering gate re-derived "did Sim's key pay" from the tool's hosting config
and whether the caller omitted the key. That cannot see a workspace or
organization BYOK key, which `injectHostedKeyIfNeeded` injects while reporting
`isUsingHostedKey: false` — the org's own key, not billable — so the derivation
called a BYOK call hosted.

The registry's verdict does propagate, by one path. On a tool with `hosting`,
`output.cost` has a single writer, `applyHostedKeyCostToResult`, and it runs only
under `isUsingHostedKey && success`. A BYOK call, a caller-keyed call, and a
failed call all leave the field absent. The gate now reads that: `hosting`
present, success, cost present. The derivation stays for the one question it
can answer before dispatch — a parameter Sim will fill is not missing.

The reading rests on hosted tools not reporting their own cost in that field,
which is true of all 131 today and now pinned by the reachability audit, so a
future hosted tool that self-reports fails CI rather than billing BYOK calls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(audit): exempt a token-response field only where the tool declares it

The reachability audit treated seven credential-derived fields as guaranteed
fillers for a required hidden parameter on any OAuth tool. Only `accessToken`
is: the resolver assigns it unconditionally. `idToken`, `instanceUrl`,
`apiDomain`, `cloudId`, `domain` and `authStyle` are assigned under
`if (data.X)` — present on some providers' credentials and absent on others —
and `credentialType` additionally only when the tool lists it in
`authoritativeParams`. Whether a credential carries one is a fact about the
provider that the resolver cannot vouch for.

The tool can. `oauth.authoritativeParams` is already the declaration that the
token response supplies the named field, and every real case — the eight
`microsoft_dynamics_365_*` tools hiding `instanceUrl` — already lists it. So a
required hidden parameter in that set is now exempt only when its tool declares
it there, generalising the rule `credentialType` alone had. A tool that hides
one without declaring it is asserting a filler the resolver may never run,
which is the shape this audit exists to reject.

Mutation-tested at the real declaration site: stripping `authoritativeParams`
from `DYNAMICS_365_OAUTH_CONFIG` fails the audit naming all eight tools with the
remedy; restoring passes. Nothing is flagged on the current tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docs Ready Ready Preview Sep 2, 2026 1:38am UTC

Request Review

@waleedlatif1 waleedlatif1 changed the title staging v0.8.18: Sep 2, 2026
@waleedlatif1 waleedlatif1 changed the title v0.8.18: v0.8.18: table ttls, perf improvements, egress management, desktop app improvements, tools APIs, permission groups enhancements, sailpoint integration, files keyword search Sep 2, 2026
@waleedlatif1
waleedlatif1 marked this pull request as ready for review September 2, 2026 02:47
@waleedlatif1
waleedlatif1 requested a review from a team as a code owner September 2, 2026 02:47
@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review (2308 files, 100 file limit).

@waleedlatif1
waleedlatif1 merged commit 5fb3c07 into main Sep 2, 2026
66 of 68 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants